package org.zhuzx.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/**
 * 使用HttpURLConnection发送HTTP请求的若干封装，支持https，使用UTF-8字符集。
 * @author	zhuzx	2017年3月11日
 */
public class NetUtil {
	
	private static final String CRLF = "\r\n";
	private static final String TWO_HYPHENS = "--";
	
	public static final String TEXT_PLAIN = "text/plain;charset=UTF-8";
	public static final String FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8";
	public static final String MULTIPART_FORM = "multipart/form-data;charset=UTF-8";
	public static final String APPLICATION_JSON = "application/json;charset=UTF-8";
	
	/**
	 * 获取连接，根据url返回普通连接或者加密连接。
	 */
	@Deprecated//完善自动重试
	private static HttpURLConnection getHttpURLConnection(String url) throws IOException {
		if (url.indexOf(' ') > 0) {
			url = url.replace(" ", "%20");
		}
		URL urlObj = new URL(url);
		HttpURLConnection conn;
		if (url.startsWith("https")) {
			HttpsURLConnection httpsConn = (HttpsURLConnection) urlObj.openConnection();
			httpsConn.setHostnameVerifier(getHostnameVerifier());
			httpsConn.setSSLSocketFactory(getSSLSocketFactory());
			conn = httpsConn;
		} else {
			conn = (HttpURLConnection) urlObj.openConnection();
		}
		conn.setRequestProperty("Connection", "Keep-Alive");
		/*
		 * 不加这个会默认发送accept=text/html, image/gif, image/jpeg, *; q=.2, *斜杠*; q=.2
		 * 造成服务器端的response.setCharacterEncoding方法失效（参见源码注释）
		 * 客户端将得到响应头为Content-Type:text/html;charset=ISO-8859-1
		 */
		conn.setRequestProperty("Accept", TEXT_PLAIN);
		/*这是Accept系列的说明，但对于字符集的处理与实际不符，底层机制未知。
			请求头字段			说明 					响应头字段
			Accept 			告知服务器发送何种媒体类型 	Content-Type
			Accept-Language 告知服务器发送何种语言 	Content-Language
			Accept-Charset 	告知服务器发送何种字符集 	Content-Type
			Accept-Encoding 告知服务器采用何种压缩方式 	Content-Encoding
		 */
		//conn.setRequestProperty("Accept-Charset", "UTF-8");//这句无效，即使在前面显式设置了Accept=text/html也无效。
		conn.setConnectTimeout(30000);
		conn.setReadTimeout(300000);//此处改小可能会报错，比如10000，baseInfoPassenger，limit 1000
		conn.setDoInput(true);
		conn.setUseCaches(false);
		return conn;
	}
	
	private static String executeRequest(String url, String bodyData, Map<String,String> headers,
			String methodType) throws IOException {
		HttpURLConnection conn = getHttpURLConnection(url);
		conn.setRequestMethod(methodType);
		byte[] bytes = null;
		if (!GeneralUtil.isBlank(bodyData)) {
			conn.setDoOutput(true);
			bytes = bodyData.getBytes(StandardCharsets.UTF_8);
			conn.setFixedLengthStreamingMode(bytes.length);
		}
		if (methodType.equals("POST") || methodType.equals("PUT")) {
			conn.setRequestProperty("Content-Type", TEXT_PLAIN);//默认值，实际可被传入的header值覆盖。
		}
		if (!GeneralUtil.isEmpty(headers)) {
			for (Map.Entry<String, String> entry : headers.entrySet()) {
				conn.setRequestProperty(entry.getKey(), entry.getValue());
			}
		}
		conn.connect();
		if (bytes != null) {
			OutputStream out = conn.getOutputStream();
			out.write(bytes);
			out.flush();
			out.close();
		}
		return getResult(conn);
	}
	
	/**
	 * @param url 可带参数
	 * @param params 可为空
	 */
	public static String get(String url, Map<String,String> params) throws IOException {
		return get(url, params, null);
	}
	
	/**
	 * @param url 可带参数
	 * @param params 可为空
	 * @param headers 可为空
	 */
	public static String get(String url, Map<String,String> params, Map<String,String> headers) throws IOException {
		url = WebUtil.transferParamsIntoUrl(url, params);
		return executeRequest(url, null, headers, "GET");
	}
	
	/**
	 * 以普通表单提交形式调用
	 * @param url 可带参数
	 * @param params 可为空
	 */
	public static String post(String url, Map<String,String> params) throws IOException {
		url = WebUtil.transferParamsIntoUrl(url, params);
		int speIndex = url.indexOf('?');
		String paramsStr = null;
		if (speIndex > -1) {
			paramsStr = url.substring(speIndex + 1);
			url = url.substring(0, speIndex);
		}
		Map<String,String> headers = new HashMap<>();
		headers.put("Content-Type", FORM_URLENCODED);
		return executeRequest(url, paramsStr, headers, "POST");
	}
	
	/**
	 * @param url 可带参数
	 * @param bodyData 数据主体，可以是任意形式，可为空。
	 * @param headers 可为空
	 */
	public static String post(String url, String bodyData, Map<String,String> headers) throws IOException {
		return executeRequest(url, bodyData, headers, "POST");
	}
	
	/**
	 * 上传文件流，可携带文本参数，以及设置额外的请求头。
	 * @param url 可带参数
	 * @param params 可为空
	 * @param headers 额外的请求头，可为空。
	 * @param ins 需要上传的文件流，可为空。
	 * @param fileNames 与文件流对应的文件名，可省略。
	 * 					若传入map则文件名必须与文件流一一对应，若传入null则以ins中的key（实体域的name）作为文件名。<br>
	 * 					注意，传入了map就必须在其中指定每个文件名，不允许部分缺省，
	 * 					以防调用者因遗漏而导致无意中发送了错误的文件名（实体域的name）。
	 */
	public static String postInputStream(String url, Map<String,String> params, Map<String,String> headers,
			Map<String,InputStream> ins, Map<String,String> fileNames) throws IOException {
		if (params == null) {
			params = new HashMap<String,String>();
		}
		url = WebUtil.transferParamsIntoMap(url, params);
		HttpURLConnection conn = getHttpURLConnection(url);
		conn.setRequestMethod("POST");
		conn.setDoOutput(true);
		if (!GeneralUtil.isEmpty(headers)) {
			for (Map.Entry<String, String> entry : headers.entrySet()) {
				conn.setRequestProperty(entry.getKey(), entry.getValue());
			}
		}
		String boundary = getBoundary(headers);
		if (GeneralUtil.isBlank(boundary)) {
			boundary = TextConstants.HTTP_ENTITY_SEPARATOR;
			String ct = GeneralUtil.getNotBlankString(headers.get("Content-Type"), MULTIPART_FORM);
			String addTo = "; boundary=" + boundary;
			conn.setRequestProperty("Content-Type", ct+addTo);
		}
		conn.connect();
		
		OutputStream out = conn.getOutputStream();
		if (!GeneralUtil.isEmpty(params)) {
			for (Map.Entry<String, String> entry : params.entrySet()) {
				if (!GeneralUtil.isBlank(entry.getValue())) {
					byte[] fieldBytes = createFieldContent(boundary, entry.getKey(), entry.getValue());
					out.write(fieldBytes);
				}
			}
		}
		if (!GeneralUtil.isEmpty(ins)) {
			if (fileNames == null) {
				fileNames = new HashMap<String,String>();
				for (String key : ins.keySet()) {
					fileNames.put(key, key);
				}
			}
			for (Map.Entry<String, InputStream> entry : ins.entrySet()) {
				byte[] fieldBytes = createFieldContent(boundary, entry.getKey(),
						fileNames.get(entry.getKey()), entry.getValue());
				out.write(fieldBytes);
			}
		}
		String end = TWO_HYPHENS + boundary + TWO_HYPHENS + CRLF;
		out.write(end.getBytes(StandardCharsets.UTF_8));
		out.flush();
		out.close();
		return getResult(conn);
	}
	
	/**
	 * @param url 可带参数
	 * @param bodyData 数据主体，可以是任意形式，可为空。
	 * @param headers 可为空
	 */
	public static String put(String url, String bodyData, Map<String,String> headers) throws IOException {
		return executeRequest(url, bodyData, headers, "PUT");
	}
	
	/**
	 * @param url 可带参数
	 * @param headers 可为空
	 */
	public static String delete(String url, Map<String,String> headers) throws IOException {
		return executeRequest(url, null, headers, "DELETE");
	}
	
	/**
	 * 获取请求头中可能已指定的实体分隔符
	 */
	private static String getBoundary(Map<String,String> headers) {
		if (!GeneralUtil.isEmpty(headers)) {
			String contentType = headers.get("Content-Type");
			if (!GeneralUtil.isBlank(contentType)) {
				String[] temp = contentType.split(" *; *");
				for (String string : temp) {
					if (string.startsWith("boundary")) {
						return string.substring(string.indexOf('=') + 1);
					}
				}
			}
		}
		return null;
	}
	
	/**
	 * 生成文本实体的字节数组
	 */
	private static byte[] createFieldContent(String boundary, String name, String value) {
		String content = TWO_HYPHENS + boundary + CRLF +
				"Content-Disposition: form-data;" + " name=\"" + name + "\"" +
				CRLF + CRLF + value + CRLF;
		return content.getBytes(StandardCharsets.UTF_8);
	}
	
	/**
	 * 生成文件流实体的字节数组
	 */
	private static byte[] createFieldContent(String boundary, String name, String filename, InputStream in) {
		if ((GeneralUtil.isBlank(filename)) != (in==null)) {
			throw new IllegalArgumentException("文件实体" + name + "缺失文件名");
		}
		String content = TWO_HYPHENS + boundary + CRLF +
				"Content-Disposition: form-data;" + " name=\"" + name + "\"" +
				"; filename=\"" + filename + "\"" +
				CRLF + "Content-Type: application/octet-stream" + CRLF + CRLF;
		byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
		byte[] inBytes = GeneralUtil.getBytes(in);
		bytes = GeneralUtil.getCombinedArray(bytes, inBytes);
		bytes = GeneralUtil.getCombinedArray(bytes, CRLF.getBytes(StandardCharsets.UTF_8));
		return bytes;
	}
	
	/**
	 * 获取连接返回的响应，若请求出错则获取错误信息。
	 */
	private static String getResult(HttpURLConnection conn) throws IOException {
		byte[] resultBytes = getResBytes(conn);
		//conn.disconnect();//是否需要手动？是否有必要手动？以下是jsoup中的两句说明：
		// per Java's documentation, this is not necessary, and precludes keepalives. However in practise,
		// connection errors will not be released quickly enough and can cause a too many open files error.
		return isGzip(conn) ? FileOperationUtil.gzipUnCompress(resultBytes) :
				new String(resultBytes, StandardCharsets.UTF_8);
	}
	
	private static byte[] getResBytes(HttpURLConnection conn) throws IOException {
		int code = conn.getResponseCode();
		InputStream resIn;
		if (code < 200 || code >= 300) {
			//System.out.println("----" + code + " " + conn.getResponseMessage() + "\nheaders:\n" + conn.getHeaderFields());
			resIn = conn.getErrorStream();
			if (resIn == null) {
				String generalDesc = "There have been no errors, the connection is not connected"
						+ " or the server sent no useful data.";
				return generalDesc.getBytes(StandardCharsets.UTF_8);
			}
		} else {
			resIn = conn.getInputStream();
		}
		return GeneralUtil.getBytes(resIn);
	}
	
	/**
	 * 寻找响应头中是否有指明压缩格式，以判断是否需要对响应信息解压缩。
	 */
	private static boolean isGzip(HttpURLConnection conn) {
		return isGzip(getResHeaders(conn));
	}
	
	/**
	 * 寻找响应头中是否有指明压缩格式，以判断是否需要对响应信息解压缩。
	 */
	@Deprecated//升级为支持各种压缩类型，乃至多值：https://cloud.tencent.com/developer/section/1189917
	private static boolean isGzip(Map<String,String> resHeaders) {
		String enco = resHeaders.get("Content-Encoding");
		if (!GeneralUtil.isBlank(enco) && enco.trim().equalsIgnoreCase("gzip")) {
			return true;
		}
		String dispo = resHeaders.get("Content-Disposition");
		if (!GeneralUtil.isBlank(dispo)) {
			String[] temp = dispo.trim().split(" *; *");
			for (String string : temp) {
				if (string.startsWith("filename") && string.endsWith(".gz")) {
					return true;
				}
			}
		}
		return false;
	}
	
	public static SSLSocketFactory getSSLSocketFactory() {
		TrustManager[] trustManager = new TrustManager[] {
				new X509TrustManager() {
					public X509Certificate[] getAcceptedIssuers() {return null;}
					public void checkClientTrusted(X509Certificate[] certs, String authType) {}
					public void checkServerTrusted(X509Certificate[] certs, String authType) {}
				}
		};
		try {
			SSLContext sslContext = SSLContext.getInstance("SSL");
			sslContext.init(null, trustManager, new SecureRandom());
			SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
			return sslSocketFactory;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}
	
	public static HostnameVerifier getHostnameVerifier() {
		HostnameVerifier hv = new HostnameVerifier() {
			public boolean verify(final String s, final SSLSession sslSession) {
				return true;
			}
		};
		return hv;
	}
	
	/**
	 * 纯请求，不对参数做任何加工。
	 * @param url 不做参数转移，原样传输。
	 * @param bytes 请求体，原样传输。
	 * @param headers 需要此参数传入Content-Type，方法内部并未默认指定。
	 * @param methodType 支持大小写，理论上支持传入："GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
	 * @return 含有2个元素的数组。第一个元素是byte[]，存放响应数据；第二个元素是LinkedHashMap<String,String>，存放响应头。
	 */
	public static Object[] executeRequestCrudely(String url, byte[] bytes, Map<String,String> headers,
			String methodType) throws IOException {
		HttpURLConnection conn = getHttpURLConnection(url);
		conn.setRequestMethod(methodType.toUpperCase());
		if (bytes != null && bytes.length > 0) {
			conn.setDoOutput(true);
			conn.setFixedLengthStreamingMode(bytes.length);
		}
		if (!GeneralUtil.isEmpty(headers)) {
			for (Map.Entry<String, String> entry : headers.entrySet()) {
				conn.setRequestProperty(entry.getKey(), entry.getValue());
			}
		}
		conn.connect();
		if (bytes != null && bytes.length > 0) {
			OutputStream out = conn.getOutputStream();
			out.write(bytes);
			out.flush();
			out.close();
		}
		return new Object[] {getResBytes(conn), getResHeaders(conn)};
	}
	
	private static Map<String,String> getResHeaders(HttpURLConnection conn) {
		Map<String,String> resHeaders = new LinkedHashMap<>();
		for (Map.Entry<String,List<String>> entry : conn.getHeaderFields().entrySet()) {
			List<String> values = entry.getValue();
			resHeaders.put(entry.getKey(), GeneralUtil.isEmpty(values) ? null :
				GeneralUtil.concatString("; ", values, false));
		}
		return resHeaders;
	}

	/**
	 * 测试ip端口连通性
	 */
	public static boolean testIpAndPort(String ip, int port) {
		Assert.mustTrue(ip.equals("localhost") || ip.matches(UnifiedConstants.REGEX_IP),
				"ip格式不正确：" + ip);
		try (Socket s = new Socket()) {
			s.connect(new InetSocketAddress(ip, port), 200);
			return s.isConnected();
		} catch (IOException ignored) {}
		return false;
	}

	/**
	 * 测试ip端口连通性
	 */
	public static boolean testIpAndPort(String ipPort) {
		String port = MicroUtil.subBehind(ipPort, ':');
		Assert.mustTrue(port.matches("\\d{1,5}"), "端口格式不正确：" + port);
		String ip = MicroUtil.removeSuffix(ipPort, ":" + port);
		return testIpAndPort(ip, Integer.parseInt(port));
	}
	
}